// CyTargetLinker,
// a Cytoscape plugin to extend biological networks with regulatory interaction
// Copyright 2011-2012 BiGCaT Bioinformatics
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
package cytargetlinker;

import giny.model.Edge;
import giny.model.Node;

import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.io.File;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;

import org.apache.commons.collections15.MultiMap;
import org.apache.commons.collections15.multimap.MultiHashMap;

import cytargetlinker.dialogs.CyTargetLinkerDialog;
import cytoscape.CyEdge;
import cytoscape.CyNetwork;
import cytoscape.CyNode;
import cytoscape.Cytoscape;
import cytoscape.data.CyAttributes;
import cytoscape.task.ui.JTaskConfig;
import cytoscape.task.util.TaskManager;
import cytoscape.view.CyNetworkView;
import cytoscape.visual.CalculatorCatalog;
import cytoscape.visual.VisualMappingManager;
import cytoscape.visual.VisualStyle;

/**
 * main class of the plugin that collects the data 
 * and distributes the different tasks to the right classes
 *
 */
public class CyTargetLinker implements PropertyChangeListener {

	private CyTargetLinkerPlugin plugin;
	private VisualStyle visualStyle;

	private MultiMap<String, Node> userIdMultiMap;
	private Map<String, Node> rinMap;
	private CyTargetLinkerObject currentObj;
	private ResultTab tab;
	private String resNetName = "";
	private Direction direction = Direction.BOTH;
	
	public enum Direction {
		SOURCE,
		TARGET,
		BOTH
	}
	
	public CyTargetLinker(CyTargetLinkerPlugin plugin) {
		this.plugin = plugin;
		Cytoscape.getPropertyChangeSupport().addPropertyChangeListener(Cytoscape.NETWORK_DESTROYED, this);
		rinMap = new HashMap<String, Node>();
		userIdMultiMap = new MultiHashMap<String, Node>();
	}
	
	/**
	 * opens the load interactions dialog
	 */
	public void openDialog() {
		userIdMultiMap.clear();
		rinMap.clear();
		currentObj = new CyTargetLinkerObject();
		CyTargetLinkerDialog dialog = new CyTargetLinkerDialog(this);
		dialog.setVisible(true);
	}
	
	/**
	 * starts the merge task (add interactions from the 
	 * RINs into the resulting network)
	 * @param rins
	 */
	public void startMerge(Map<String, File> rins) {
		MergeTask task = new MergeTask(this, rins);
		JTaskConfig jTaskConfig = new JTaskConfig();
		jTaskConfig.setOwner(Cytoscape.getDesktop());
		jTaskConfig.displayCloseButton(true);
		jTaskConfig.displayCancelButton(true);
		jTaskConfig.displayStatus(true);
		jTaskConfig.setAutoDispose(false);
		
		// Execute Task in New Thread; pops open JTask Dialog Box.
		TaskManager.executeTask(task, jTaskConfig);
	}

	public void setUserNetwork(CyNetwork network, String idAttribute) {
		if(network != null && idAttribute != null && !idAttribute.equals("")) {
			currentObj.setUserNetwork(network);
			currentObj.setIdAttribute(idAttribute);
		}
	}
	
	/**
	 * loads and opens the rins
	 */
	public List<CyNetwork> loadNetworks(Map<String,File> rins) {
		List<CyNetwork> list = new ArrayList<CyNetwork>();
		int i = 0;
		for (String str : rins.keySet()) {
			CyNetwork currentNetwork = Cytoscape.createNetworkFromFile(str, false);
			list.add(currentNetwork);

			CyAttributes rinAttributes = Cytoscape.getNetworkAttributes();
			String databaseName = rinAttributes.getStringAttribute(currentNetwork.getIdentifier(),"Database");
			RegulatoryInteractionNetwork network = new RegulatoryInteractionNetwork(databaseName);
			network.setNetwork(currentNetwork);
			network.setColor(Preferences.getInstance().getColors().get(i));
			network.setNewInteractions(0);
			currentObj.getRIN(network);
			i++;
		}
		return list;
	}
	
	/**
	 * adds regulatory interactions to the resulting network
	 */
	@SuppressWarnings("unchecked")
	public void mergeNetworks() {
		
		checkNetName();
		
		// copy user network into new network
		CyNetwork newNetwork = Cytoscape.createNetwork(currentObj.getUserNetwork().nodesList(), currentObj.getUserNetwork().edgesList(), resNetName);
		currentObj.setCreateNetwork(newNetwork);
		
		// get attribute(s) for the User node(s).....
		CyAttributes cyNodeAttrs = Cytoscape.getNodeAttributes();
		
		// add a new attribute for the network nodes in the new network
		addAttributeToPanel(currentObj.getCreateNetwork().nodesList(), cyNodeAttrs);

		// Making a HashMap for id & node(s) in the new network.....
		getUserNetworkIds(currentObj.getCreateNetwork().nodesList(), cyNodeAttrs);
		
		for(int i = 0; i < currentObj.getRINs().size(); i++) {
			CyNetwork network = currentObj.getRINs().get(i).getNetwork();
			// get node(s) forN the miRNA Network.....
			List<Node> nodeListRIN = network.nodesList();
			
			// get attribute(s) for the miRNA node(s).....
			CyAttributes rinAttributes = Cytoscape.getNetworkAttributes();
			String targetIdAttribute = rinAttributes.getStringAttribute(network.getIdentifier(), "TargetIdAttribute");
			String sourceIdAttribute = rinAttributes.getStringAttribute(network.getIdentifier(), "SourceIdAttribute");
			String databaseName = rinAttributes.getStringAttribute(network.getIdentifier(), "Database");

			CyAttributes rinNodeAttr = Cytoscape.getNodeAttributes();
			
			// Making a HashMap for entrezIds & node(s) in the miRNA Network.....
			rinMap = new HashMap<String, Node>();
			for (Node node : nodeListRIN) {
				String targetId = rinNodeAttr.getStringAttribute(node.getIdentifier(), targetIdAttribute);
				String sourceId = rinNodeAttr.getStringAttribute(node.getIdentifier(), sourceIdAttribute);
				String sourceData = rinNodeAttr.getStringAttribute(node.getIdentifier(), databaseName);

				rinMap.put(targetId, node);
				rinMap.put(sourceId, node);
				rinMap.put(sourceData, node);
			}

			addNewNodesToUserNetwork(network, currentObj.getCreateNetwork());
		}
		
		for(RegulatoryInteractionNetwork network : currentObj.getRINs()) {
			Cytoscape.destroyNetwork(network.getNetwork());
		}
		
		CyNetworkView view = Cytoscape.getCurrentNetworkView();
		
		// get the VisualMappingManager and CalculatorCatalog
		VisualMappingManager manager = Cytoscape.getVisualMappingManager();
		CalculatorCatalog catalog = manager.getCalculatorCatalog();

		// check to see if a visual style with this name already exists
		visualStyle = catalog.getVisualStyle(Preferences.getInstance().getVsName());
		if (visualStyle == null) {
			// if not, create it and add it to the catalog
			CyTargetLinkerVisualStyle style = new CyTargetLinkerVisualStyle();

			visualStyle = style.createVisualStyle(this);
			catalog.addVisualStyle(visualStyle);
		}

		view.setVisualStyle(visualStyle.getName()); // not strictly necessary

		// actually apply the visual style
		manager.setVisualStyle(visualStyle);
	}
	
	/**
	 * checks if network name is already given
	 */
	private void checkNetName() {
		Set<CyNetwork> networks = Cytoscape.getNetworkSet();
		for(CyNetwork net : networks) {
			if(net.getTitle().equals(resNetName)) {
				setResNetName(resNetName + "_1");
				checkNetName();
			}
		}
	}

	/**
	 * saves all node identifiers from the user network in the
	 * userIdMultiMap
	 */
	@SuppressWarnings("unchecked")
	private void getUserNetworkIds(List<Node> nodes, CyAttributes cyNodeAttrs) {
		userIdMultiMap = new MultiHashMap<String, Node>();
		
		for (Node node : nodes) {
			Set<String> values = new HashSet<String>();
			
			byte type = cyNodeAttrs.getType(currentObj.getIdAttribute());
			switch(type) {
			case CyAttributes.TYPE_STRING:
				values.add(cyNodeAttrs.getStringAttribute(node.getIdentifier(), currentObj.getIdAttribute()));
				break;
			case CyAttributes.TYPE_SIMPLE_LIST:
				values.addAll(cyNodeAttrs.getListAttribute(node.getIdentifier(), currentObj.getIdAttribute()));
				break;
			default:
				//warning
			}
			
			for(String id : values) {
				if(id != null && !"".equals(id)) {
					userIdMultiMap.put(id, node);
				}
			}
		}
	}

	/**
	 * adds the node type attribute
	 * network node = node that existed in the user network
	 * new node = node that was added because it has a regulatory interaction in a RIN
	 */
	private void addAttributeToPanel(List<Node> nodesList, CyAttributes cyNodeAttrs) {
		// adding the new attribute to the User node(s).....
		for (Node node : nodesList) {
			
			// check if node has value in attribute NodeType
			String value = cyNodeAttrs.getStringAttribute(node.getIdentifier(),  Preferences.getInstance().getNodeTypeAttribute());
			
			// if not = set value to NetworkNode
			if(value == null || value.equals("")) {
				if(isNetworkNode(node, cyNodeAttrs)) {
					cyNodeAttrs.setAttribute(node.getIdentifier(),  Preferences.getInstance().getNodeTypeAttribute(), Preferences.getInstance().getNetworkNodeValue());
					cyNodeAttrs.setAttribute(node.getIdentifier(),  "label", getLabel(node, cyNodeAttrs));
				} else {
					cyNodeAttrs.setAttribute(node.getIdentifier(),  Preferences.getInstance().getNodeTypeAttribute(), Preferences.getInstance().getGroupNodeValue());
				}
			} else {
				
				// if value is not NewNode - set value to NetworkNode
				if(!value.equals(Preferences.getInstance().getNewNodeValue())) {
					if(isNetworkNode(node, cyNodeAttrs)) {
						cyNodeAttrs.setAttribute(node.getIdentifier(),  Preferences.getInstance().getNodeTypeAttribute(), Preferences.getInstance().getNetworkNodeValue());
						cyNodeAttrs.setAttribute(node.getIdentifier(),  "label", getLabel(node, cyNodeAttrs));
					} else {
						cyNodeAttrs.setAttribute(node.getIdentifier(),  Preferences.getInstance().getNodeTypeAttribute(), Preferences.getInstance().getGroupNodeValue());
					}
				}
			}
		}

		// Inform others via property change event.
		Cytoscape.firePropertyChange(Cytoscape.ATTRIBUTES_CHANGED, null, null);
	}
	
	/**
	 * if a node has an attribute "label" or "Label"
	 * it is used to label the node
	 * otherwise the user would see the often unreadable identifiers
	 */
	private String getLabel(Node node, CyAttributes cyNodeAttrs) {
		String label = cyNodeAttrs.getStringAttribute(node.getIdentifier(), "label");
		if(label == null) {
			label = cyNodeAttrs.getStringAttribute(node.getIdentifier(), "Label");
			if(label == null) {
				String name = cyNodeAttrs.getStringAttribute(node.getIdentifier(), "canonicalName");
				if(name != null && !name.equals("")) {
					label = name;
				} else {
					label = node.getIdentifier();
				}
			}
		}
		return label;
	}
	
	/**
	 * checks if a node exists in the user network
	 */
	public boolean isNetworkNode(Node node, CyAttributes cyNodeAttrs) {
		String nodeTypeAttribute = cyNodeAttrs.getStringAttribute(node.getIdentifier(),  Preferences.getInstance().getNodeTypeAttribute());
		
		if(nodeTypeAttribute == null) {
			if(!isGroupNode(node, cyNodeAttrs)) {
				return true;
			}
		} else {
			// if value is not NewNode - set value to NetworkNode
			if(!nodeTypeAttribute.equals(Preferences.getInstance().getNewNodeValue())) {
				if(!isGroupNode(node, cyNodeAttrs)) {
					return true;
				}
			}
		}
		return false;
	}

	/**
	 * checks if a node is a GPML group node
	 */
	private boolean isGroupNode(Node node, CyAttributes cyNodeAttrs) {
		String groupStyle = cyNodeAttrs.getStringAttribute(node.getIdentifier(), "GroupStyle");
		if(groupStyle != null && (groupStyle.equals("Group") || groupStyle.equals("Complex"))) {
			return true;
		}
		String anchorStyle = cyNodeAttrs.getStringAttribute(node.getIdentifier(), "StartLineType");
		if(anchorStyle != null) {
			return true;
		}
		return false;
	}

	/**
	 * adds the new nodes to the user network
	 */
	private void addNewNodesToUserNetwork(CyNetwork rin, CyNetwork resultingNetwork) {
		for (String id : userIdMultiMap.keySet()) {
			
			// check if id is in miRNA network
			if (rinMap.containsKey(id)) {
				// get nodes
				// Node nodeUserNetwork = mapGenesUserNetwork.get(id);
				Node node = rinMap.get(id);

				// check for neighbors in miRNANetwork
				@SuppressWarnings("rawtypes")
				List neighbors = rin.neighborsList(node);
				// and iterate over the neighbors
				for (Iterator ni = neighbors.iterator(); ni.hasNext();) {
					CyNode neighbor = (CyNode) ni.next();
					// get the view on this neighbor

					
					// find out if neighbor or node is source/target in target network
					List<Node> list = new ArrayList<Node>();
					list.add(node);
					list.add(neighbor);
					List<Edge> listConnectingEdges = rin.getConnectingEdges(list);
					
					Boolean isSource = true;
					if(listConnectingEdges.size() > 0) {
						if(listConnectingEdges.get(0).getSource().getIdentifier().equals(neighbor.getIdentifier())) {
							isSource = false;
						}
					}	
					
					// Creating & Adding new edges/connections to the
					// nodeUserNetwork
					Collection<Node> targets = userIdMultiMap.get(id);
					for(Node n : targets) {
						CyEdge newEdge = null;
						String edgeType = currentObj.getRIN(rin).getName();
						CyAttributes cyNodeAttrs = Cytoscape.getNodeAttributes();
						if(!isSource) {
							if(direction.equals(Direction.BOTH) || direction.equals(Direction.TARGET)) {
								// Adding miRNA nodes to the userNetwork
								resultingNetwork.addNode(neighbor);
								newEdge = Cytoscape.getCyEdge(neighbor.getIdentifier(), "miRNAlink", n.getIdentifier(), edgeType);
							}
						} else {
							if(direction.equals(Direction.BOTH) || direction.equals(Direction.SOURCE)) {
								// Adding miRNA nodes to the userNetwork
								resultingNetwork.addNode(neighbor);
								newEdge = Cytoscape.getCyEdge(n.getIdentifier(), "miRNAlink", neighbor.getIdentifier(), edgeType);
							}
						}
						if(newEdge != null) {
							resultingNetwork.addEdge(newEdge);
							currentObj.getRIN(rin).setNewInteractions(currentObj.getRIN(rin).getNewInteractions()+1);
						}
					}

					// giving a new attribute to the neighbour new nodes
					CyAttributes cyNeighborAttrs = Cytoscape.getNodeAttributes();
					cyNeighborAttrs.setAttribute(neighbor.getIdentifier(),  "label", getLabel(neighbor, cyNeighborAttrs));
					cyNeighborAttrs.setAttribute(neighbor.getIdentifier(),  Preferences.getInstance().getNodeTypeAttribute(), Preferences.getInstance().getNewNodeValue());
				}
				Cytoscape.createNetworkView(resultingNetwork, "CyTargetLinkerView");
			}
		}
	}

	public CyTargetLinkerObject getCurrentObj() {
		return currentObj;
	}

	/**
	 * adds the tab to the result panel
	 */
	public void createColorTab() {
		tab = new ResultTab(this);
		tab.fillTab();
	}

	/**
	 * removes the tab in the result panel 
	 * if the network is destroyed
	 */
	@Override
	public void propertyChange(PropertyChangeEvent event) {
		if (event.getPropertyName().equalsIgnoreCase(Cytoscape.NETWORK_DESTROYED)) {
			if(currentObj != null && currentObj.getCreateNetwork() != null) {
				String id = currentObj.getCreateNetwork().getIdentifier();
				if(currentObj.getCreateNetwork().getIdentifier() == null) {
					id = currentObj.getCreateNetwork().getTitle();
				}
				if(event.getNewValue().equals(id)) {
					tab.removeTab();
				}
			}
		}
	}

	public String getResNetName() {
		return resNetName;
	}

	public void setResNetName(String resNetName) {
		this.resNetName = resNetName;
	}

	public void setDirection(Direction direction) {
		this.direction = direction;
	}

	public CyTargetLinkerPlugin getPlugin() {
		return plugin;
	}
}
